package dataarts

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"reflect"
	"strings"

	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

	"github.com/chnsz/golangsdk"
	"github.com/chnsz/golangsdk/openstack/dayu/v1/connections"

	"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/common"
	"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config"
	"github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils"
)

// @API DataArtsStudio POST /v1/{project_id}/data-connections
// @API DataArtsStudio GET /v1/{project_id}/data-connections/{data_connection_id}
// @API DataArtsStudio GET /v1/{project_id}/data-connections
// @API DataArtsStudio PUT /v1/{project_id}/data-connections/{data_connection_id}
// @API DataArtsStudio DELETE /v1/{project_id}/data-connections/{data_connection_id}
// @API DataArtsStudio POST /v1/{project_id}/data-connections/validation

// ResourceDataConnection is the impl of huaweicloud_dataarts_studio_data_connection
func ResourceDataConnection() *schema.Resource {
	return &schema.Resource{
		CreateContext: resourceDataConnectionCreate,
		ReadContext:   resourceDataConnectionRead,
		UpdateContext: resourceDataConnectionUpdate,
		DeleteContext: resourceDataConnectionDelete,

		Importer: &schema.ResourceImporter{
			StateContext: resourceDataConnectionImportState,
		},

		Schema: map[string]*schema.Schema{
			"region": {
				Type:        schema.TypeString,
				Optional:    true,
				Computed:    true,
				ForceNew:    true,
				Description: `The region where the data connection is located.`,
			},
			"workspace_id": {
				Type:        schema.TypeString,
				Required:    true,
				ForceNew:    true,
				Description: `The ID of the workspace to which the data connection belongs.`,
			},
			"name": {
				Type:        schema.TypeString,
				Required:    true,
				Description: `The data connection name.`,
			},
			"type": {
				Type:        schema.TypeString,
				Required:    true,
				Description: `The data connection type.`,
			},
			"config": {
				Type:         schema.TypeString,
				Optional:     true,
				ValidateFunc: validation.StringIsJSON,
				DiffSuppressFunc: func(_, o, n string, _ *schema.ResourceData) bool {
					oldRaws := unmarshalConfigContent(o)
					newRaws := unmarshalConfigContent(n)
					// The API response will return an attribute 'update_time', which is a random string generated by
					// the create or update operation. This field is not included in the configuration comparison.
					delete(oldRaws, "update_time")

					return reflect.DeepEqual(oldRaws, newRaws)
				},
				Description: `The dynamic configuration for the specified type of data connection.`,
			},
			"agent_id": {
				Type:        schema.TypeString,
				Optional:    true,
				Computed:    true,
				Description: `The agent ID.`,
			},
			"agent_name": {
				Type:         schema.TypeString,
				Optional:     true,
				Computed:     true,
				RequiredWith: []string{"agent_id"},
				Description:  `The agent name.`,
			},
			"env_type": {
				Type:        schema.TypeInt,
				Optional:    true,
				Computed:    true,
				Description: `The data connection mode.`,
			},
			"qualified_name": {
				Type:        schema.TypeString,
				Computed:    true,
				Description: `The qualified name of this data connection.`,
			},
			"created_at": {
				Type:        schema.TypeString,
				Computed:    true,
				Description: `The creation time of the data connection.`,
			},
			"created_by": {
				Type:        schema.TypeString,
				Computed:    true,
				Description: `The name of the data connection creator.`,
			},
		},
	}
}

func unmarshalConfigContent(configVal string) map[string]interface{} {
	parseResult := make(map[string]interface{})
	err := json.Unmarshal([]byte(configVal), &parseResult)
	if err != nil {
		log.Printf("[ERROR] Invalid type of the dynamic configuration content, it is not JSON format")
		return nil
	}
	return parseResult
}

func validateDataConnection(client *golangsdk.ServiceClient, d *schema.ResourceData) error {
	validateOpts := connections.ValidateOpts{
		WorkspaceId: d.Get("workspace_id").(string),
		DwName:      d.Get("name").(string),
		DwType:      d.Get("type").(string),
		DwConfig:    unmarshalConfigContent(d.Get("config").(string)),
		AgentId:     d.Get("agent_id").(string),
		AgentName:   d.Get("agent_name").(string),
		EnvType:     d.Get("env_type").(int),
	}
	resp, err := connections.Validate(client, validateOpts)
	if err != nil {
		return fmt.Errorf("the pre-check request of the DataArts Studio data connection is failed: %s", err)
	}
	if !resp.IsSuccess {
		return fmt.Errorf("the pre-check of the DataArts Studio data connection is failed: %s", resp.Message)
	}
	return nil
}

func resourceDataConnectionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
	conf := meta.(*config.Config)
	region := conf.GetRegion(d)
	client, err := conf.DataArtsV1Client(region)
	if err != nil {
		return diag.Errorf("error creating DataArts Studio v1 client: %s", err)
	}

	err = validateDataConnection(client, d)
	if err != nil {
		return diag.FromErr(err)
	}
	var (
		workspaceId = d.Get("workspace_id").(string)
		createOpts  = connections.CreateOpts{
			WorkspaceId: workspaceId,
			DataSourceVos: []connections.DataSourceVo{
				{
					DwName:    d.Get("name").(string),
					DwType:    d.Get("type").(string),
					DwConfig:  unmarshalConfigContent(d.Get("config").(string)),
					AgentId:   d.Get("agent_id").(string),
					AgentName: d.Get("agent_name").(string),
					EnvType:   d.Get("env_type").(int),
				},
			},
		}
	)

	connectionId, err := connections.Create(client, createOpts)
	if err != nil {
		return diag.Errorf("error creating DataArts Studio data connection: %s", err)
	}

	d.SetId(connectionId)
	return resourceDataConnectionRead(ctx, d, meta)
}

func resourceDataConnectionRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
	conf := meta.(*config.Config)
	region := conf.GetRegion(d)
	client, err := conf.DataArtsV1Client(region)
	if err != nil {
		return diag.Errorf("error creating DataArts Studio v1 client: %s", err)
	}

	var (
		workspaceId  = d.Get("workspace_id").(string)
		connectionId = d.Id()
	)
	resp, err := connections.Get(client, workspaceId, connectionId)
	if err != nil {
		return common.CheckDeletedDiag(d, ParseDataConnectionError(err), "DataArts Studio data connection")
	}
	mErr := multierror.Append(nil,
		d.Set("region", region),
		d.Set("name", resp.DwName),
		d.Set("type", resp.DwType),
		d.Set("agent_id", resp.AgentId),
		d.Set("agent_name", resp.AgentName),
		d.Set("env_type", resp.EnvType),
		d.Set("qualified_name", resp.QualifiedName),
		d.Set("created_at", utils.FormatTimeStampRFC3339(int64(resp.CreateTime/1000), false)),
		d.Set("created_by", resp.CreateUser),
	)
	jsonFilter, err := json.Marshal(resp.DwConfig)
	if err != nil {
		mErr = multierror.Append(mErr, fmt.Errorf("unable to convert the dynamic configuration, it is not JSON format"))
	} else {
		mErr = multierror.Append(mErr, d.Set("config", string(jsonFilter)))
	}

	if err := mErr.ErrorOrNil(); err != nil {
		return diag.Errorf("error saving DataArts Studio data connection (%s) fields: %s", connectionId, err)
	}
	return nil
}

func resourceDataConnectionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
	conf := meta.(*config.Config)
	region := conf.GetRegion(d)
	client, err := conf.DataArtsV1Client(region)
	if err != nil {
		return diag.Errorf("error creating DataArts Studio v1 client: %s", err)
	}

	err = validateDataConnection(client, d)
	if err != nil {
		return diag.FromErr(err)
	}
	var (
		connectionId = d.Id()
		workspaceId  = d.Get("workspace_id").(string)
		updateOpts   = connections.UpdateOpts{
			WorkspaceId:  workspaceId,
			ConnectionId: connectionId,
			DataSourceVos: []connections.DataSourceVo{
				{
					DwName:    d.Get("name").(string),
					DwType:    d.Get("type").(string),
					DwConfig:  unmarshalConfigContent(d.Get("config").(string)),
					AgentId:   d.Get("agent_id").(string),
					AgentName: d.Get("agent_name").(string),
					EnvType:   d.Get("env_type").(int),
				},
			},
		}
	)

	err = connections.Update(client, updateOpts)
	if err != nil {
		return diag.Errorf("error updating DataArts Studio data connection (%s): %s", connectionId, err)
	}
	return resourceDataConnectionRead(ctx, d, meta)
}

func resourceDataConnectionDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
	conf := meta.(*config.Config)
	region := conf.GetRegion(d)
	client, err := conf.DataArtsV1Client(region)
	if err != nil {
		return diag.Errorf("error creating DataArts Studio v1 client: %s", err)
	}

	var (
		workspaceId  = d.Get("workspace_id").(string)
		connectionId = d.Id()
	)
	err = connections.Delete(client, workspaceId, connectionId)
	if err != nil {
		return diag.Errorf("error deleting DataArts Studio data connection (%s): %s", connectionId, err)
	}
	return nil
}

func resourceDataConnectionImportState(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData,
	error) {
	parts := strings.Split(d.Id(), "/")
	if len(parts) < 2 {
		return nil, fmt.Errorf("invalid format specified for import ID, want '<workspace_id>/<name>', but got '%s'", d.Id())
	}

	cfg := meta.(*config.Config)
	region := cfg.GetRegion(d)
	client, err := cfg.DataArtsV1Client(region)
	if err != nil {
		return []*schema.ResourceData{d}, fmt.Errorf("error creating DataArts Studio v1 client: %s", err)
	}

	var (
		workspaceId = parts[0]
		name        = parts[1]
		opts        = connections.ListOpts{
			WorkspaceId: workspaceId,
			Name:        name,
		}
	)
	connectionList, err := connections.List(client, opts)
	if err != nil {
		return []*schema.ResourceData{d}, fmt.Errorf("error query DataArts Studio data connections: %s", err)
	}
	if len(connectionList) < 1 {
		return []*schema.ResourceData{d}, fmt.Errorf("data connection (%s) not found: %s", name, err)
	}

	d.SetId(connectionList[0].DwId)
	return []*schema.ResourceData{d}, d.Set("workspace_id", workspaceId)
}

type dataConncectionError struct {
	// Error code
	ErrCode string `json:"error_code"`
	// Error message
	ErrMessage string `json:"error_msg"`
}

// ParseDataConnectionError is a method for parsing 404 errors in both cases where the workspace does not exist and the
// data connection does not exist.
func ParseDataConnectionError(respErr error) error {
	var apiError struct {
		ErrorContents []dataConncectionError `json:"errors"`
	}

	if errCode, ok := respErr.(golangsdk.ErrDefault400); ok {
		pErr := json.Unmarshal(errCode.Body, &apiError)
		if len(apiError.ErrorContents) < 1 {
			return respErr
		}
		parseResult := apiError.ErrorContents[0]
		if pErr == nil && (parseResult.ErrCode == "DAYU.3601" &&
			parseResult.ErrMessage == "You have no permission in asset.") {
			return golangsdk.ErrDefault404(errCode)
		}
		if pErr == nil && (parseResult.ErrCode == "DAYU.1005" &&
			parseResult.ErrMessage == "The data connection does not exist.") {
			return golangsdk.ErrDefault404(errCode)
		}
	}
	return respErr
}
